frontend/pages/e/[uuid]/details.tsx (view raw)
1import moment from 'moment';
2import Linkify from 'linkify-react';
3import Tooltip from '@mui/material/Tooltip';
4import IconButton from '@mui/material/IconButton';
5import Box from '@mui/material/Box';
6import Link from '@mui/material/Link';
7import Card from '@mui/material/Card';
8import Container from '@mui/material/Container';
9import TextField from '@mui/material/TextField';
10import Typography from '@mui/material/Typography';
11import TuneIcon from '@mui/icons-material/Tune';
12import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
13import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
14import {useTheme} from '@mui/material/styles';
15import {DatePicker} from '@mui/x-date-pickers/DatePicker';
16import {PropsWithChildren, useState} from 'react';
17import {useTranslation} from 'next-i18next';
18import pageUtils from '../../../lib/pageUtils';
19import DetailsLink from '../../../containers/DetailsLink';
20import ShareEvent from '../../../containers/ShareEvent';
21import PlaceInput from '../../../containers/PlaceInput';
22import LangSelector from '../../../components/LangSelector';
23import usePermissions from '../../../hooks/usePermissions';
24import useEventStore from '../../../stores/useEventStore';
25import useToastStore from '../../../stores/useToastStore';
26import EventLayout, {TabComponent} from '../../../layouts/Event';
27import {
28 EventByUuidDocument,
29 useUpdateEventMutation,
30} from '../../../generated/graphql';
31import {langLocales} from '../../../locales/constants';
32import {getLocaleForLang} from '../../../lib/getLocale';
33
34interface Props {
35 eventUUID: string;
36 announcement?: string;
37}
38
39const Page = (props: PropsWithChildren<Props>) => {
40 return <EventLayout {...props} Tab={DetailsTab} />;
41};
42
43const DetailsTab: TabComponent<Props> = ({}) => {
44 const {t} = useTranslation();
45 const {
46 userPermissions: {canEditEventDetails},
47 } = usePermissions();
48 const theme = useTheme();
49 const [updateEvent] = useUpdateEventMutation();
50 const addToast = useToastStore(s => s.addToast);
51 const setEventUpdate = useEventStore(s => s.setEventUpdate);
52 const event = useEventStore(s => s.event);
53 const [isEditing, setIsEditing] = useState(false);
54
55 if (!event) return null;
56
57 const hasGeoloc = event.latitude && event.longitude;
58
59 const onSave = async e => {
60 try {
61 const {uuid, ...data} = event;
62 delete data.linkedEvent;
63 delete data.isReturnEvent;
64 const {
65 id,
66 travels,
67 waitingPassengers,
68 __typename,
69 administrators,
70 passengers,
71 ...input
72 } = data;
73 await updateEvent({
74 variables: {
75 uuid,
76 eventUpdate: {
77 ...input,
78 },
79 },
80 refetchQueries: ['eventByUUID'],
81 });
82 setIsEditing(false);
83 } catch (error) {
84 console.error(error);
85 addToast(t('event.errors.cant_update'));
86 }
87 };
88
89 const modifyButton = isEditing ? (
90 <Tooltip
91 title={t('event.details.save')}
92 sx={{
93 position: 'absolute',
94 top: theme.spacing(2),
95 right: theme.spacing(2),
96 }}
97 >
98 <IconButton color="primary" onClick={onSave}>
99 <CheckCircleOutlineIcon />
100 </IconButton>
101 </Tooltip>
102 ) : (
103 <Tooltip
104 title={t('event.details.modify')}
105 sx={{
106 position: 'absolute',
107 top: theme.spacing(2),
108 right: theme.spacing(2),
109 }}
110 >
111 <IconButton color="primary" onClick={() => setIsEditing(true)}>
112 <TuneIcon />
113 </IconButton>
114 </Tooltip>
115 );
116
117 return (
118 <Box
119 sx={{
120 position: 'relative',
121 }}
122 >
123 <Container
124 sx={{
125 p: 4,
126 mt: 6,
127 mb: 11,
128 mx: 0,
129 [theme.breakpoints.down('md')]: {
130 p: 2,
131 },
132 }}
133 >
134 <Card
135 sx={{
136 position: 'relative',
137 maxWidth: '100%',
138 width: '480px',
139 p: 2,
140 }}
141 >
142 <Typography variant="h4" pb={2}>
143 {t('event.details')}
144 </Typography>
145 {canEditEventDetails() && modifyButton}
146 {(isEditing || event.name) && (
147 <Box pt={2} pr={1.5}>
148 <Typography variant="overline">
149 {t('event.fields.name')}
150 </Typography>
151 <Typography>
152 {isEditing ? (
153 <TextField
154 size="small"
155 fullWidth
156 value={event.name}
157 onChange={e => setEventUpdate({name: e.target.value})}
158 name="name"
159 id="EditEventName"
160 />
161 ) : (
162 <Typography id="EventName">{event.name}</Typography>
163 )}
164 </Typography>
165 </Box>
166 )}
167 {(isEditing || event.date) && (
168 <Box pt={2} pr={1.5}>
169 <Typography variant="overline">
170 {t('event.fields.date')}
171 </Typography>
172 {isEditing ? (
173 <Typography>
174 <DatePicker
175 slotProps={{
176 textField: {
177 size: 'small',
178 id: `EditEventDate`,
179 fullWidth: true,
180 placeholder: t('event.fields.date_placeholder'),
181 },
182 }}
183 format="DD/MM/YYYY"
184 value={moment(event.date)}
185 onChange={date =>
186 setEventUpdate({
187 date: !date ? null : moment(date).format('YYYY-MM-DD'),
188 })
189 }
190 />
191 </Typography>
192 ) : (
193 <Box position="relative">
194 <Typography id="EventDate">
195 {moment(event.date).format('DD/MM/YYYY')}
196 </Typography>
197 </Box>
198 )}
199 </Box>
200 )}
201 {(isEditing || event.address) && (
202 <Box pt={2} pr={1.5}>
203 <Typography variant="overline">
204 {t('event.fields.address')}
205 </Typography>
206 {isEditing ? (
207 <PlaceInput
208 place={event.address}
209 latitude={event.latitude}
210 longitude={event.longitude}
211 onSelect={({place, latitude, longitude}) =>
212 setEventUpdate({
213 address: place,
214 latitude,
215 longitude,
216 })
217 }
218 />
219 ) : (
220 <Box position="relative">
221 <Typography
222 id="EventAddress"
223 title={t`placeInput.noCoordinates`}
224 sx={{
225 pr: 3,
226 display: 'inline-flex',
227 alignItems: 'center',
228 columnGap: 1,
229 }}
230 >
231 <Link
232 target="_blank"
233 rel="noreferrer"
234 href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
235 event.address
236 )}`}
237 onClick={e => e.preventDefault}
238 >
239 {event.address}
240 </Link>
241 {!hasGeoloc && (
242 <InfoOutlinedIcon fontSize="small" color="warning" />
243 )}
244 </Typography>
245 </Box>
246 )}
247 </Box>
248 )}
249 {(isEditing || event.description) && (
250 <Box pt={2} pr={1.5}>
251 <Typography variant="overline">
252 {t('event.fields.description')}
253 </Typography>
254 {isEditing ? (
255 <Typography>
256 <TextField
257 fullWidth
258 multiline
259 maxRows={4}
260 inputProps={{maxLength: 250}}
261 value={event.description || ''}
262 onChange={e =>
263 setEventUpdate({description: e.target.value})
264 }
265 id={`EditEventDescription`}
266 name="description"
267 />
268 </Typography>
269 ) : (
270 <Typography
271 id="EventDescription"
272 sx={{pr: 3, whiteSpace: 'pre-line'}}
273 >
274 <Linkify options={{render: DetailsLink}}>
275 {event.description}
276 </Linkify>
277 </Typography>
278 )}
279 </Box>
280 )}
281 {(isEditing || event.lang) && (
282 <Box pt={2} pr={1.5}>
283 <Typography variant="overline">
284 {t('event.fields.lang')}
285 </Typography>
286 {isEditing ? (
287 <LangSelector
288 value={event.lang}
289 onChange={lang => setEventUpdate({lang})}
290 />
291 ) : (
292 <Typography id="EventLang" sx={{pr: 3}}>
293 {langLocales[event.lang]}
294 </Typography>
295 )}
296 </Box>
297 )}
298 {!isEditing && (
299 <ShareEvent
300 title={`Caroster ${event.name}`}
301 sx={{width: '100%', mt: 2}}
302 />
303 )}
304 </Card>
305 </Container>
306 </Box>
307 );
308};
309
310export const getServerSideProps = pageUtils.getServerSideProps(
311 async (context, apolloClient) => {
312 const {uuid} = context.query;
313 const {host = ''} = context.req.headers;
314 let event = null;
315
316 // Fetch event
317 try {
318 const {data} = await apolloClient.query({
319 query: EventByUuidDocument,
320 variables: {uuid},
321 });
322 event = data?.eventByUUID?.data;
323 } catch (error) {
324 return {
325 notFound: true,
326 };
327 }
328
329 const description = await getLocaleForLang(
330 event?.attributes?.lang,
331 'meta.description'
332 );
333
334 return {
335 props: {
336 eventUUID: uuid,
337 metas: {
338 title: event?.attributes?.name || '',
339 description,
340 url: `https://${host}${context.resolvedUrl}`,
341 },
342 },
343 };
344 }
345);
346export default Page;